using System;
using System.Globalization;
using System.Linq;
using GammaJul.ReSharper.GammaXaml.Psi;
using GammaJul.ReSharper.GammaXaml.Psi.Xaml;
using JetBrains.Annotations;
using JetBrains.ReSharper.Feature.Services.Xaml.Bulbs;
using JetBrains.ReSharper.Intentions.Extensibility;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.Parsing;
using JetBrains.ReSharper.Psi.Tree;
using JetBrains.ReSharper.Psi.Xaml;
using JetBrains.ReSharper.Psi.Xaml.Impl;
using JetBrains.ReSharper.Psi.Xaml.Impl.Resolve;
using JetBrains.ReSharper.Psi.Xaml.Impl.Tree;
using JetBrains.ReSharper.Psi.Xaml.Tree;
using JetBrains.ReSharper.Psi.Xml.Parsing;
using JetBrains.ReSharper.Psi.Xml.Tree;
using JetBrains.Text;

namespace GammaJul.ReSharper.GammaXaml.ContextActions.Xaml {

	/// <summary>
	/// Base class for context actions that manipulate a row or column definition.
	/// </summary>
	public abstract class GridDefinitionItemBase : BulbActionBase {
		private readonly XamlContextActionDataProvider _provider;
		private readonly GridDefinitionType _definitionType;

		/// <summary>
		/// Gets the <see cref="XamlContextActionDataProvider"/> for this action.
		/// </summary>
		[NotNull]
		public XamlContextActionDataProvider Provider {
			get { return _provider; }
		}

		/// <summary>
		/// Gets the type of grid definition.
		/// </summary>
		public GridDefinitionType DefinitionType {
			get { return _definitionType; }
		}

		protected sealed class DefinitionInfo {
			private readonly IXamlObjectElement _definition;
			private readonly IXamlPropertyElement _definitionsSetter;
			private readonly IXamlObjectElement _grid;
			private readonly ITypeElement _gridType;

			[NotNull]
			public IXamlObjectElement Definition {
				get { return _definition; }
			}

			[NotNull]
			public IXamlPropertyElement DefinitionsSetter {
				get { return _definitionsSetter; }
			}

			[NotNull]
			public IXamlObjectElement Grid {
				get { return _grid; }
			}

			[NotNull]
			public ITypeElement GridType {
				get { return _gridType; }
			}

			internal DefinitionInfo([NotNull] IXamlObjectElement definition, [NotNull] IXamlPropertyElement definitionsSetter, [NotNull] IXamlObjectElement grid, [NotNull] ITypeElement gridType) {
				_definition = definition;
				_definitionsSetter = definitionsSetter;
				_grid = grid;
				_gridType = gridType;
			}
		}

		/// <summary>
		/// Gets the grid definition at the current caret position.
		/// </summary>
		/// <param name="provider">The XAML data provider.</param>
		/// <param name="definitionType">The type of grid definition.</param>
		/// <returns>A <see cref="DefinitionInfo"/> at caret if it's available, otherwise <c>null</c>.</returns>
		[CanBeNull]
		protected static DefinitionInfo GetDefinitionInfoAtCaret([NotNull] XamlContextActionDataProvider provider, GridDefinitionType definitionType) {
			if (provider == null)
				throw new ArgumentNullException("provider");

			var identifier = provider.FindNodeAtCaret<IXmlIdentifier>();
			if (identifier == null)
				return null;

			// verify that we are on <XxxDefinition>
			var definition = identifier.GetContainingNode<IXamlObjectElement>();
			if (definition == null || definition.GetNameNode() != identifier)
				return null;

			IXamlTypeCache xamlTypeCache = XamlTypeCache.GetCache(provider.PsiModule);

			ITypeElement defTypeElement = definition.TypeReference.ResolvedType();
			if (defTypeElement == null || !defTypeElement.IsDescendantOf(xamlTypeCache.GridDefinition(definitionType).GetTypeElement()))
				return null;

			// verify that the <XxxDefinition> parent is <Grid.XxxDefinitions>
			var definitions = definition.Parent as IXamlPropertyElement;
			if (definitions == null || definitions.PropertyName != definitionType.ToString() + "Definitions")
				return null;

			ITypeElement gridTypeElement = xamlTypeCache.Grid().GetTypeElement();
			if (gridTypeElement == null || !Equals(definitions.ContainingTypeReference.ResolvedType(), gridTypeElement))
				return null;

			// verify that <Grid.XxxDefinitions> parent is <Grid>
			var grid = definitions.Parent as IXamlObjectElement;
			if (grid == null)
				return null;

			ITypeElement possibleGridTypeElement = grid.TypeReference.ResolvedType();
			if (possibleGridTypeElement == null || !possibleGridTypeElement.IsDescendantOf(gridTypeElement))
				return null;

			return new DefinitionInfo(definition, definitions, grid, gridTypeElement);
		}

		/// <summary>
		/// Gets the grid definition at the current caret position.
		/// </summary>
		/// <returns>A <see cref="DefinitionInfo"/> at caret if it's available, otherwise <c>null</c>.</returns>
		[CanBeNull]
		protected DefinitionInfo GetDefinitionInfoAtCaret() {
			return GetDefinitionInfoAtCaret(_provider, _definitionType);
		}

		/// <summary>
		/// Gets whether there is a matching definition at the current caret position.
		/// </summary>
		/// <param name="provider">The XAML data provider.</param>
		/// <param name="definitionType">The type of grid definition.</param>
		/// <returns><c>true</c> if there's a valid definition at caret position, otherwise <c>null</c>.</returns>
		public static bool HasDefinitionAtCaret([NotNull] XamlContextActionDataProvider provider, GridDefinitionType definitionType) {
			return GetDefinitionInfoAtCaret(provider, definitionType) != null;
		}

		/// <summary>
		/// Updates Grid.Row/Column and Grid.RowSpan/ColumnSpan (based on <see cref="DefinitionType"/>) on every children of a Grid.
		/// </summary>
		/// <param name="grid">The grid to update.</param>
		/// <param name="gridType">The type representing Grid.</param>
		/// <param name="newDefValueFactory">A function taking the old Row/Column value and returning the new Row/Column value.</param>
		/// <param name="newSpanValueFactory">
		/// A function taking the old Row/Column value as the first parameter,
		/// the old RowSpan/ColumnSpan value as the second parameter,
		/// and returning the new RowSpan/ColumnSpan value.
		/// </param>
		protected void UpdateGridChildrenDefinitionValue([NotNull] IXamlObjectElement grid, [NotNull] ITypeElement gridType, [NotNull] Func<int, int> newDefValueFactory, [NotNull] Func<int, int, int> newSpanValueFactory) {
			if (grid == null)
				throw new ArgumentNullException("grid");
			if (gridType == null)
				throw new ArgumentNullException("gridType");
			if (newDefValueFactory == null)
				throw new ArgumentNullException("newDefValueFactory");
			if (newSpanValueFactory == null)
				throw new ArgumentNullException("newSpanValueFactory");

			// find the Grid.SetXxx/SetXxxSpan methods
			IMethod defSetter;
			IMethod spanSetter;
			FindDefAndSpanSetters(gridType, out defSetter, out spanSetter);
			if (defSetter == null || spanSetter == null)
				return;

			XmlTokenTypes xmlTokenTypes = XmlTokenTypes.GetInstance<XamlLanguage>();
			XamlElementFactory elementFactory = XamlElementFactory.GetElementFactory(grid);

			// iterate through the Grid children and change their Xxx/XxxSpan values
			foreach (var gridChild in grid.InnerTags.OfType<IXamlObjectElement>()) {

				// get the Row/Column value
				IXmlToken defValueToken;
				int defValue;
				if (!TryParseSetterValue(gridChild.GetPropertySetter(defSetter), 0, out defValueToken, out defValue)) {
					// we cannot parse the Row/Column value; even if we have a valid RowSpan/ColumnSpan value,
					// it won't have any meaning without Row/Column so we just ignore this child
					continue;
				}
				
				// replace the Row/Column value if needed
				int newDefValue = Math.Max(newDefValueFactory(defValue), 0);
				if (newDefValue != defValue)
					UpdateOrCreateValue(defValueToken, newDefValue, defSetter, gridChild, elementFactory, xmlTokenTypes);

				// get the RowSpan/ColumnSpan value
				IXmlToken spanValueToken;
				int spanValue;
				if (!TryParseSetterValue(gridChild.GetPropertySetter(spanSetter), 1, out spanValueToken, out spanValue))
					continue;

				// replace the RowSpan/ColumnSpan value if needed
				int newSpanValue = Math.Max(newSpanValueFactory(defValue, spanValue), 1);
				if (newSpanValue != spanValue)
					UpdateOrCreateValue(spanValueToken, newSpanValue, spanSetter, gridChild, elementFactory, xmlTokenTypes);
			}
		}

		/// <summary>
		/// Finds Grid.Row/Column and Grid.RowSpan/ColumnSpan setters.
		/// </summary>
		/// <param name="gridType">A type representing Grid.</param>
		/// <param name="defSetter">When the function returns, a method representing Grid.Row/Column.</param>
		/// <param name="spanSetter">When the function returns, a method representing Grid.RowSpan/ColumnSpan.</param>
		private void FindDefAndSpanSetters([NotNull] ITypeElement gridType, [CanBeNull] out IMethod defSetter, [CanBeNull] out IMethod spanSetter) {
			defSetter = null;
			spanSetter = null;
			
			string defMethodName = _definitionType.ToString().ToDpAttachedSetterName();
			string spanMethodName = defMethodName + "Span";
			IXamlTypeCache xamlTypeCache = XamlTypeCache.GetCache(Provider.PsiModule);
			int count = 0;

			foreach (IMethod method in gridType.Methods) {
				if (defSetter == null && method.ShortName == defMethodName && method.IsDpAttachedSetter(xamlTypeCache)) {
					defSetter = method;
					if (++count == 2)
						return;
				}
				else if (spanSetter == null && method.ShortName == spanMethodName && method.IsDpAttachedSetter(xamlTypeCache)) {
					spanSetter = method;
					if (++count == 2)
						return;
				}
			}
		}

		/// <summary>
		/// Tries to parse a property setter value.
		/// </summary>
		/// <param name="setter">The property setter</param>
		/// <param name="noValue">The value to return if <paramref name="setter"/> is <c>null</c>.</param>
		/// <param name="valueToken">When the function returns, the value token. Only has a meaning if the function returns <c>true</c>.</param>
		/// <param name="value">When the function returns, the parsed value. Only has a meaning if the function returns <c>true</c>.</param>
		/// <returns><c>true</c> if <paramref name="setter"/> is <c>null</c> or has an integer value, <c>false</c> otherwise.</returns>
		private static bool TryParseSetterValue([CanBeNull] IPropertySetter setter, int noValue, out IXmlToken valueToken, out int value) {
			if (setter == null) {
				valueToken = null;
				value = noValue;
				return true;
			}

			valueToken = setter.GetValueToken();
			if (valueToken == null) {
				value = noValue;
				return false;
			}

			return Int32.TryParse(valueToken.GetText(), out value);
		}

		/// <summary>
		/// Updates or create a grid attached setter.
		/// </summary>
		/// <param name="valueToken">The value token to update.</param>
		/// <param name="newValue">The new value.</param>
		/// <param name="setter">The grid setter.</param>
		/// <param name="owner">The tag owning the setter.</param>
		/// <param name="elementFactory">A factory used to create XML nodes.</param>
		/// <param name="xmlTokenTypes">A <see cref="XmlTokenTypes"/> instance.</param>
		private void UpdateOrCreateValue([CanBeNull] ITreeNode valueToken, int newValue, [NotNull] IMethod setter,
			[NotNull] IXmlTag owner, [NotNull] IXmlElementFactory elementFactory, [NotNull] XmlTokenTypes xmlTokenTypes) {
			
			// there was a definition value previously, update it
			if (valueToken != null)
				valueToken.ReplaceBy(xmlTokenTypes.TEXT.Create(new StringBuffer(newValue.ToString(CultureInfo.InvariantCulture))));
					
			// otherwise create it
			else {
				string attrString = String.Format(CultureInfo.InvariantCulture, "Grid.{0}=\"{1}\"", setter.GetExpectedDpName(), newValue);
				IXmlAttribute defAttr = elementFactory.CreateAttribute(attrString, _provider.PsiServices, _provider.PsiModule, true);
				owner.AddAttributeBefore(defAttr, null);
			}
		}

		protected GridDefinitionItemBase([NotNull] XamlContextActionDataProvider provider, GridDefinitionType definitionType) {
			if (provider == null)
				throw new ArgumentNullException("provider");
			_provider = provider;
			_definitionType = definitionType;
		}
	}

}